package com.liuqianfei.markme.rules.block;

import com.liuqianfei.markme.common.Utils;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.markme.rules.AbstractState;
import com.liuqianfei.markme.rules.IAction;
import com.liuqianfei.markme.rules.StateBlock;
import com.liuqianfei.util.StringUtil;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Lists
 * @author liuqianfei
 */
public class ListAction implements IAction
{
    @Override
    public boolean parse(AbstractState state, boolean silent, int startLine, int endLine)
    {
        char ch;
        int contentStart;
        int i;
        int indent;
        int indentAfterMarker;
        int initial;
        boolean isOrdered;
        int[] itemLines;
        int l;
        int[] listLines;
        int listTokIdx;
        char markerCharCode;
        int markerValue = 0;
        int max;
        int nextLine;
        int offset;
        int oldIndent;
        int oldLIndent;
        String oldParentType;
        int oldTShift;
        boolean oldTight;
        int pos;
        int posAfterMarker;
        boolean prevEmptyEnd;
        int start;
        boolean terminate;
        List<IAction> terminatorRules;
        Token token;
        boolean isTerminatingParagraph = false;
        boolean tight = true;

        // limit conditions when list can interrupt
        // a paragraph (validation mode only)
        if (silent && "paragraph".equals(state.getParentType()))
        {
            // Next list item should still terminate previous list item;
            //
            // This code can fail if plugins use blkIndent as well as lists,
            // but I hope the spec gets fixed long before that happens.
            //
            if (state.gettShift(startLine) >= state.getBlkIndent())
                isTerminatingParagraph = true;
        }

        // Detect list type and position after marker
        if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0)
        {
            isOrdered = true;
            start = state.getbMarks(startLine) + state.gettShift(startLine);
            markerValue = Integer.parseInt(StringUtil.substr(state.getSrc(), start, posAfterMarker - start - 1));

            // If we're starting a new ordered list right after
            // a paragraph, it should start with 1.
            if (isTerminatingParagraph && markerValue != 1)
                return false;
        }
        else if ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0)
        {
            isOrdered = false;

        }
        else
        {
            return false;
        }

        // If we're starting a new unordered list right after
        // a paragraph, first line should not be empty.
        if (isTerminatingParagraph)
            if (state.skipSpaces(posAfterMarker) >= state.geteMarks(startLine))
                return false;

        // We should terminate list on style change. Remember first one to compare.
        markerCharCode = state.getSrc().charAt(posAfterMarker - 1);

        // For validation mode we can terminate immediately
        if (silent)
            return true;

        // Start list
        listTokIdx = state.getTokens().size();

        if (isOrdered)
        {
            token       = state.push("ordered_list_open", "ol", 1);
            if (markerValue != 1)
            {
                Map<String, String> attrs = new HashMap<>();
                attrs.put("start", String.valueOf(markerValue));
                token.attrs = attrs;
            }

        }
        else
        {
            token       = state.push("bullet_list_open", "ul", 1);
        }

        token.map    = listLines = new int[]{startLine, 0};
        token.markup = String.valueOf(markerCharCode);

        //
        // Iterate list items
        //

        nextLine = startLine;
        prevEmptyEnd = false;
        terminatorRules = state.getMd().parserBlock.ruler.getRules("list");

        oldParentType = state.getParentType();
        state.setParentType("list");

        while (nextLine < endLine)
        {
            pos = posAfterMarker;
            max = state.geteMarks(nextLine);

            initial = offset = state.getsCount(nextLine) + posAfterMarker - (state.getbMarks(startLine) + state.gettShift(startLine));

            while (pos < max)
            {
                ch = state.getSrc().charAt(pos);

                if (Utils.isSpace(ch))
                {
                    if (ch == 0x09)
                        offset += 4 - (offset + state.getBsCount(nextLine)) % 4;
                    else
                        offset++;
                }
                else
                {
                    break;
                }

                pos++;
            }

            contentStart = pos;

            if (contentStart >= max)
            {
                // trimming space in "-    \n  3" case, indent is 1 here
                indentAfterMarker = 1;
            }
            else
            {
                indentAfterMarker = offset - initial;
            }

            // If we have more than 4 spaces, the indent is 1
            // (the rest is just indented code block)
            if (indentAfterMarker > 4)
                indentAfterMarker = 1;

            // "  -  test"
            //  ^^^^^ - calculating total length of this thing
            indent = initial + indentAfterMarker;

            // Run subparser & write tokens
            token        = state.push("list_item_open", "li", 1);
            token.markup = String.valueOf(markerCharCode);
            token.map    = itemLines = new int[]{startLine, 0};

            oldIndent = state.getBlkIndent();
            oldTight = state.isTight();
            oldTShift = state.gettShift(startLine);
            oldLIndent = state.getsCount(startLine);
            state.setBlkIndent(indent);
            state.setTight(true);
            state.settShift(startLine, contentStart - state.getbMarks(startLine));
            state.setsCount(startLine, offset);

            if (contentStart >= max && state.isEmpty(startLine + 1))
            {
                // workaround for this case
                // (list item is empty, list terminates before "foo"):
                // ~~~~~~~~
                //   -
                //
                //     foo
                // ~~~~~~~~
                state.setLine(Math.min(state.getLine() + 2, endLine));
            }
            else
            {
                state.getMd().parserBlock.tokenize((StateBlock) state, startLine, endLine);
            }

            // If any of list item is tight, mark list as tight
            if (!state.isTight() || prevEmptyEnd)
            {
                tight = false;
            }
            // Item become loose if finish with empty line,
            // but we should filter last element, because it means list finish
            prevEmptyEnd = (state.getLine() - startLine) > 1 && state.isEmpty(state.getLine() - 1);

            state.setBlkIndent(oldIndent);
            state.settShift(startLine, oldTShift);
            state.setsCount(startLine, oldLIndent);
            state.setTight(oldTight);

            token        = state.push("list_item_close", "li", -1);
            token.markup = String.valueOf(markerCharCode);

            nextLine = startLine = state.getLine();
            itemLines[1] = nextLine;
//            contentStart = state.getbMarks(startLine);

            if (nextLine >= endLine)
                break;

            //
            // Try to check if list is terminated or continued.
            //
            if (state.getsCount(nextLine) < state.getBlkIndent())
                break;

            // fail if terminating block found
            terminate = false;
            for (i = 0, l = terminatorRules.size(); i < l; i++)
            {
                if (terminatorRules.get(i).parse(state, true, nextLine, endLine))
                {
                    terminate = true;
                    break;
                }
            }
            if (terminate)
                break;

            // fail if list has another type
            if (isOrdered)
            {
                posAfterMarker = skipOrderedListMarker(state, nextLine);
                if (posAfterMarker < 0)
                    break;
            }
            else
            {
                posAfterMarker = skipBulletListMarker(state, nextLine);
                if (posAfterMarker < 0)
                    break;
            }

            if (markerCharCode != state.getSrc().charAt(posAfterMarker - 1))
                break;
        }

        // Finilize list
        if (isOrdered)
        {
            token = state.push("ordered_list_close", "ol", -1);
        }
        else
        {
            token = state.push("bullet_list_close", "ul", -1);
        }
        token.markup = String.valueOf(markerCharCode);

        listLines[1] = nextLine;
        state.setLine(nextLine);

        state.setParentType(oldParentType);

        // mark paragraphs tight if needed
        if (tight)
            markTightParagraphs(state, listTokIdx);

        return true;
    }

    // Search `[-+*][\n ]`, returns next pos arter marker on success
    // or -1 on fail.
    private int skipBulletListMarker(AbstractState state, int startLine)
    {
        int pos, max;
        char marker, ch;

        pos = state.getbMarks(startLine) + state.gettShift(startLine);
        max = state.geteMarks(startLine);

        marker = state.getSrc().charAt(pos++);
        // Check bullet
        if (marker != 0x2A/* * */
                && marker != 0x2D/* - */
                && marker != 0x2B/* + */)
            return -1;

        if (pos < max)
        {
            ch = state.getSrc().charAt(pos);

            if (!Utils.isSpace(ch))
            {
                // " -test " - is not a list item
                return -1;
            }
        }

        return pos;
    }

    // Search `\d+[.)][\n ]`, returns next pos arter marker on success
    // or -1 on fail.
    private int skipOrderedListMarker(AbstractState state, int startLine)
    {
        char ch;
        int start = state.getbMarks(startLine) + state.gettShift(startLine), pos = start, max = state.geteMarks(startLine);

        // List marker should have at least 2 chars (digit + dot)
        if (pos + 1 >= max)
            return -1;

        ch = state.getSrc().charAt(pos++);

        if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */)
            return -1;

        for (;;)
        {
            // EOL -> fail
            if (pos >= max)
                return -1;

            ch = state.getSrc().charAt(pos++);

            if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */)
            {

                // List marker should have no more than 9 digits
                // (prevents integer overflow in browsers)
                if (pos - start >= 10)
                    return -1;

                continue;
            }

            // found valid marker
            if (ch == 0x29/* ) */ || ch == 0x2e/* . */)
                break;

            return -1;
        }

        if (pos < max)
        {
            ch = state.getSrc().charAt(pos);

            if (!Utils.isSpace(ch))
            {
                // " 1.test " - is not a list item
                return -1;
            }
        }
        return pos;
    }

    private void markTightParagraphs(AbstractState state, int idx)
    {
        int i, l, level = state.getLevel() + 2;

        for (i = idx + 2, l = state.getTokens().size() - 2; i < l; i++)
        {
            if (state.getToken(i).level == level && "paragraph_open".equals(state.getToken(i).type))
            {
                state.getToken(i + 2).hidden = true;
                state.getToken(i).hidden = true;
                i += 2;
            }
        }
    }
}
